/*
 * MidiParser.java
 *
 * Created on June 27, 2007, 3:29 AM
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.midi;
import java.util.List;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.SysexMessage;
import tal.drivechain.chain.ChainNode;
import tal.drivechain.parsing.Tokenizer;
import tal.drivechain.parsing.ParsingAid;
import tal.drivechain.expression.dcExpression;
import tal.drivechain.variable.types.ArrayType;
import tal.drivechain.variable.types.DataType;
import tal.drivechain.variable.Variable;

/**
 *
 * @author Main
 */
public class MidiParser {
    public final static int NO_DEFAULT_VALUE = -100;
    public final static int NO_VALUE = -200;
    private int command;
    private dcExpression data1 = null;
    private dcExpression data2 = null;
    private dcExpression channel = null;
    SerializableShortMessage outmsg = new SerializableShortMessage();
    SerializableSysexMessage outsysex = new SerializableSysexMessage();

    /** Creates a new instance of MidiParser 
     *  format: cmd/data1/data2/channel
     *  or:     sysex/varname
     
     */
    public MidiParser(String def) throws IllegalArgumentException {
        parseDef(def);
    }

    private void parseDef(String def) throws IllegalArgumentException {
        int cmd;
        
        //divide the rule according to the / chars and determine it's format according to the command type
        Tokenizer ruleTokenizer = new Tokenizer(def, '/');
        String[] parts = new String[ruleTokenizer.getTokens().length];
        int i=0;
        while (ruleTokenizer.hasMoreTokens()) {
            parts[i] = ruleTokenizer.nextToken().toLowerCase();
            i++;
        }
        cmd = MidiUtility.commandNameToCode(parts[0]);
        if (cmd==ShortMessage.NOTE_ON || cmd==ShortMessage.NOTE_OFF) {
            if (parts.length>=1 && parts[1]!=null)
                parts[1] = MidiUtility.parseNotes(parts[1]);
        }

        if (cmd==ShortMessage.PITCH_BEND || cmd==ShortMessage.CHANNEL_PRESSURE || cmd==ShortMessage.PROGRAM_CHANGE) {
            // needs to do a special job for pitchbend...
            if (parts.length>1 && !parts[1].equals("def")) {
                if (ParsingAid.isNumber(parts[1])) parts[1] = "${" + parts[1] + "}";
                data1 = new dcExpression(parts[1], DataType.INT);
            }
            else data1 = new dcExpression("${" + getDefaultData1(cmd) + "}", DataType.INT);
            if (parts.length>2 && !parts[2].equals("def")) {
                if (ParsingAid.isNumber(parts[2])) parts[2] = "${" + parts[2] + "}";
                channel = new dcExpression(parts[2], DataType.INT);
            }
            else channel = new dcExpression("${" + getDefaultChannel(cmd) + "}", DataType.INT);
        } else if (cmd==SysexMessage.SYSTEM_EXCLUSIVE) {
            Variable var = Variable.parseVariable("sysex", null, parts[1], true);
            byte[] sysexData;
            if (var.getDataType() instanceof ArrayType) {
                List data = (List)var.getInternalValue();
                sysexData = new byte[data.size()];
                if (var.getDataType().matches(DataType.INT, DataType.MATCH_ARRAY))
                    for (int x=0; x<sysexData.length; x++)
                        sysexData[x]=((Integer)data.get(x)).byteValue();
                else if (var.getDataType().matches(DataType.LONG, DataType.MATCH_ARRAY))
                    for (int x=0; x<sysexData.length; x++)
                        sysexData[x]=((Long)data.get(x)).byteValue();
                else if (var.getDataType().matches(DataType.BOOLEAN, DataType.MATCH_ARRAY))
                    for (int x=0; x<sysexData.length; x++)
                        sysexData[x]=(byte)((Boolean)data.get(x)?1:0);
                else if (var.getDataType().matches(DataType.FLOAT, DataType.MATCH_ARRAY))
                    for (int x=0; x<sysexData.length; x++)
                        sysexData[x]=((Float)data.get(x)).byteValue();                
                else throw new IllegalArgumentException("Variable of type " + var.getDataType().getName() + "[] can't be assigned to sysex message.");
            } else {
               if (var.getDataType().matches(DataType.STRING)) {
                   sysexData = ((String)var.getInternalValue()).getBytes();
               } else {
                   sysexData = new byte[1];
                    if (var.getDataType().matches(DataType.INT))
                        sysexData[0]=((Integer)var.getInternalValue()).byteValue();
                    else if (var.getDataType().matches(DataType.LONG))
                        sysexData[0]=((Long)var.getInternalValue()).byteValue();
                    else if (var.getDataType().matches(DataType.BOOLEAN))
                        sysexData[0]=(byte)((Boolean)var.getInternalValue()?1:0);
                    else if (var.getDataType().matches(DataType.FLOAT))
                        sysexData[0]=((Float)var.getInternalValue()).byteValue();                
                    else throw new IllegalArgumentException("Variable of type " + var.getDataType().getName() + " can't be assigned to sysex message.");
               }
            }
            try {
                outsysex.setMessage(SysexMessage.SYSTEM_EXCLUSIVE, sysexData, sysexData.length);
            } catch (InvalidMidiDataException e) {
                throw new IllegalArgumentException(e.getMessage());
            }
        } else {
            if (parts.length>1 && !parts[1].equals("def")) {
                if (ParsingAid.isNumber(parts[1])) parts[1] = "${" + parts[1] + "}";
                data1 = new dcExpression(parts[1], DataType.INT);
            } else data1 = new dcExpression("${" + getDefaultData1(cmd) + "}", DataType.INT);
            if (parts.length>2 && !parts[2].equals("def")) {
                if (ParsingAid.isNumber(parts[2])) parts[2] = "${" + parts[2] + "}";
                data2 = new dcExpression(parts[2], DataType.INT);
            } else data2 = new dcExpression("${" + getDefaultData2(cmd) + "}", DataType.INT);
            if (parts.length>3 && !parts[3].equals("def")) {
                if (ParsingAid.isNumber(parts[3])) parts[3] = "${" + parts[3] + "}";
                channel = new dcExpression(parts[3], DataType.INT);
            } else channel = new dcExpression("${" + getDefaultChannel(cmd) + "}", DataType.INT);
        }
        this.command = cmd;
    }

    // has default values for each command type. if no default throw exception.
    private int getDefaultData2(int command) throws IllegalArgumentException {
        switch (command) {
            case ShortMessage.NOTE_OFF: return 64;
            case ShortMessage.NOTE_ON: return 100;
            case ShortMessage.CONTROL_CHANGE: throw new IllegalArgumentException("Message with command " + command + " doesn't have a default data2 value.");
            case ShortMessage.POLY_PRESSURE: return 100;
            default: return NO_VALUE;
        }
    }

    private int getDefaultData1(int command) throws IllegalArgumentException {
        switch (command) {
            case ShortMessage.CHANNEL_PRESSURE: return 100;
            case ShortMessage.NOTE_OFF: throw new IllegalArgumentException("Message with command " + command + " doesn't have a default data1 value.");
            case ShortMessage.NOTE_ON: throw new IllegalArgumentException("Message with command " + command + " doesn't have a default data1 value.");
            case ShortMessage.PITCH_BEND: return 8191;
            case ShortMessage.POLY_PRESSURE: throw new IllegalArgumentException("Message with command " + command + " doesn't have a default data1 value.");
            case ShortMessage.PROGRAM_CHANGE: throw new IllegalArgumentException("Message with command " + command + " doesn't have a default data1 value.");
            default: return NO_VALUE;
        }
    }
    
    private int getDefaultChannel(int command) throws IllegalArgumentException {
        switch (command) {
            case ShortMessage.CHANNEL_PRESSURE:
            case ShortMessage.PITCH_BEND:
                throw new IllegalArgumentException("Message with command " + command + " doesn't have a default channel value.");
            default: return 0;
        }
    }

    public int getCombinedDataBytes(ChainNode context) throws IllegalArgumentException {
        if (command==ShortMessage.PITCH_BEND) return getData1(context);
        else return (int)MidiUtility.combineDataBytes(getData1(context), getData2(context));
    }

    public dcExpression getExpressionData1() { return data1; }
    public dcExpression getExpressionData2() { return data2; }
    public dcExpression getExpressionChannel() { return channel; }
    
    public int getData1(ChainNode context) throws IllegalArgumentException{
        if (data1==null) throw new IllegalArgumentException("Midi message with command byte " + command + " doesn't have a data1 byte.");
        int d1 = (Integer)data1.evaluate(context);
        if (d1>127) d1 = 127;
        else if (d1<0) d1 = 0;
        if (d1==NO_VALUE) throw new IllegalArgumentException("Midi message with command byte " + command + " doesn't have a data1 byte.");
        else return d1;
    }
    
    public int getData2(ChainNode context) {
        if (data2==null) throw new IllegalArgumentException("Midi message with command byte " + command + " doesn't have a data2 byte.");
        int d2 = (Integer)data2.evaluate(context);
        if (d2>127) d2 = 127;
        else if (d2<0) d2 = 0;
        if (d2==NO_VALUE) throw new IllegalArgumentException("Midi message with command byte " + command + " doesn't have a data2 byte.");
        else return d2;

    }
    
    public int getChannel(ChainNode context) {
        if (channel==null) throw new IllegalArgumentException("Midi message with command byte " + command + " doesn't have a channel byte.");
        int c = (Integer)channel.evaluate(context);
        if (c>15) c = 15;
        else if (c<0) c = 0;
        if (c==NO_VALUE) throw new IllegalArgumentException("Midi message with command byte " + command + " doesn't have a channel byte.");
        else return c;

    }
    
    public int getCommand() {
        return command;
    }
    
    public MidiMessage getMidiMessage(ChainNode context) throws IllegalArgumentException {
        try {
            if (command==SysexMessage.SYSTEM_EXCLUSIVE) {
                return outsysex;
            } else if (command>=0xF8 && command<=0xFF) { // realtime message
                outmsg.setMessage(command);
                return outmsg;
            } else if (command>=0xF1 && command<=0xF6) {
                outmsg.setMessage(command, getData1(context), getData2(context));
                return outmsg;
            } else if (command==ShortMessage.PITCH_BEND) {
                int d1 = (Integer)data1.evaluate(context);
                short data = (short)d1;
                outmsg.setMessage(command, getChannel(context), MidiUtility.uncombineDataBytes(data)[0], MidiUtility.uncombineDataBytes(data)[1]);
                return outmsg;
            } else {
                outmsg.setMessage(command, getChannel(context), getData1(context), getData2(context));
                return outmsg;
            }
        } catch (InvalidMidiDataException e) {
            throw new IllegalArgumentException(e.getMessage());
        }
    }
    
    @Override
    public String toString() {
        return MidiUtility.commandCodeToString(command) + (data1!=null?("/" + data1.toString()):"") + (data2!=null?("/" + data2.toString()):"") + (channel!=null?("/" + channel.toString()):"");
    }
    
    public String toString(ChainNode context) {
        return MidiUtility.midiToString(getMidiMessage(context));
    }
}
